// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 1996-2014, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */

package com.ibm.icu.util;

import java.util.Date;

/**
 * <b>Note:</b> The Holiday framework is a technology preview. Despite its age, is still draft API,
 * and clients should treat it as such.
 *
 * <p>A Holiday subclass which represents holidays that occur a fixed number of days before or after
 * Easter. Supports both the Western and Orthodox methods for calculating Easter.
 *
 * @draft ICU 2.8 (retainAll)
 */
public class EasterHoliday extends Holiday {
    /**
     * Construct a holiday that falls on Easter Sunday every year
     *
     * @param name The name of the holiday
     * @draft ICU 2.8
     */
    public EasterHoliday(String name) {
        super(name, new EasterRule(0, false));
    }

    /**
     * Construct a holiday that falls a specified number of days before or after Easter Sunday each
     * year.
     *
     * @param daysAfter The number of days before (-) or after (+) Easter
     * @param name The name of the holiday
     * @draft ICU 2.8
     */
    public EasterHoliday(int daysAfter, String name) {
        super(name, new EasterRule(daysAfter, false));
    }

    /**
     * Construct a holiday that falls a specified number of days before or after Easter Sunday each
     * year, using either the Western or Orthodox calendar.
     *
     * @param daysAfter The number of days before (-) or after (+) Easter
     * @param orthodox Use the Orthodox calendar?
     * @param name The name of the holiday
     * @draft ICU 2.8
     */
    public EasterHoliday(int daysAfter, boolean orthodox, String name) {
        super(name, new EasterRule(daysAfter, orthodox));
    }

    /**
     * Shrove Tuesday, aka Mardi Gras, 48 days before Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday SHROVE_TUESDAY = new EasterHoliday(-48, "Shrove Tuesday");

    /**
     * Ash Wednesday, start of Lent, 47 days before Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday ASH_WEDNESDAY = new EasterHoliday(-47, "Ash Wednesday");

    /**
     * Palm Sunday, 7 days before Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday PALM_SUNDAY = new EasterHoliday(-7, "Palm Sunday");

    /**
     * Maundy Thursday, 3 days before Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday MAUNDY_THURSDAY = new EasterHoliday(-3, "Maundy Thursday");

    /**
     * Good Friday, 2 days before Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday GOOD_FRIDAY = new EasterHoliday(-2, "Good Friday");

    /**
     * Easter Sunday
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday EASTER_SUNDAY = new EasterHoliday(0, "Easter Sunday");

    /**
     * Easter Monday, 1 day after Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday EASTER_MONDAY = new EasterHoliday(1, "Easter Monday");

    /**
     * Ascension, 39 days after Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday ASCENSION = new EasterHoliday(39, "Ascension");

    /**
     * Pentecost (aka Whit Sunday), 49 days after Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday PENTECOST = new EasterHoliday(49, "Pentecost");

    /**
     * Whit Sunday (aka Pentecost), 49 days after Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday WHIT_SUNDAY = new EasterHoliday(49, "Whit Sunday");

    /**
     * Whit Monday, 50 days after Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday WHIT_MONDAY = new EasterHoliday(50, "Whit Monday");

    /**
     * Corpus Christi, 60 days after Easter
     *
     * @draft ICU 2.8
     */
    public static final EasterHoliday CORPUS_CHRISTI = new EasterHoliday(60, "Corpus Christi");
}

class EasterRule implements DateRule {
    public EasterRule(int daysAfterEaster, boolean isOrthodox) {
        this.daysAfterEaster = daysAfterEaster;
        if (isOrthodox) {
            calendar.setGregorianChange(new Date(Long.MAX_VALUE));
        }
    }

    /** Return the first occurrence of this rule on or after the given date */
    @Override
    public Date firstAfter(Date start) {
        return doFirstBetween(start, null);
    }

    /**
     * Return the first occurrence of this rule on or after the given start date and before the
     * given end date.
     */
    @Override
    public Date firstBetween(Date start, Date end) {
        return doFirstBetween(start, end);
    }

    /** Return true if the given Date is on the same day as Easter */
    @Override
    public boolean isOn(Date date) {
        synchronized (calendar) {
            calendar.setTime(date);
            int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);

            calendar.setTime(computeInYear(calendar.getTime(), calendar));

            return calendar.get(Calendar.DAY_OF_YEAR) == dayOfYear;
        }
    }

    /** Return true if Easter occurs between the two dates given */
    @Override
    public boolean isBetween(Date start, Date end) {
        return firstBetween(start, end) != null; // TODO: optimize?
    }

    private Date doFirstBetween(Date start, Date end) {
        // System.out.println("doFirstBetween: start   = " + start.toString());
        // System.out.println("doFirstBetween: end     = " + end.toString());

        synchronized (calendar) {
            // Figure out when this holiday lands in the given year
            Date result = computeInYear(start, calendar);

            // System.out.println("                result  = " + result.toString());

            // We might have gotten a date that's in the same year as "start", but
            // earlier in the year.  If so, go to next year
            if (result.before(start)) {
                calendar.setTime(start);
                calendar.get(Calendar.YEAR); // JDK 1.1.2 bug workaround
                calendar.add(Calendar.YEAR, 1);

                // System.out.println("                Result before start, going to next year: "
                //                        + calendar.getTime().toString());

                result = computeInYear(calendar.getTime(), calendar);
                // System.out.println("                result  = " + result.toString());
            }

            if (end != null && !result.before(end)) {
                // System.out.println("Result after end, returning null");
                return null;
            }
            return result;
        }
    }

    /**
     * Compute the month and date on which this holiday falls in the year containing the date
     * "date". First figure out which date Easter lands on in this year, and then add the offset for
     * this holiday to get the right date.
     *
     * <p>The algorithm here is taken from the <a
     * href="http://www.faqs.org/faqs/calendars/faq/">Calendar FAQ</a>.
     */
    private Date computeInYear(Date date, GregorianCalendar cal) {
        if (cal == null) cal = calendar;

        synchronized (cal) {
            cal.setTime(date);

            int year = cal.get(Calendar.YEAR);
            int g = year % 19; // "Golden Number" of year - 1
            int i = 0; // # of days from 3/21 to the Paschal full moon
            int j = 0; // Weekday (0-based) of Paschal full moon

            if (cal.getTime().after(cal.getGregorianChange())) {
                // We're past the Gregorian switchover, so use the Gregorian rules.
                int c = year / 100;
                int h = (c - c / 4 - (8 * c + 13) / 25 + 19 * g + 15) % 30;
                i = h - (h / 28) * (1 - (h / 28) * (29 / (h + 1)) * ((21 - g) / 11));
                j = (year + year / 4 + i + 2 - c + c / 4) % 7;
            } else {
                // Use the old Julian rules.
                i = (19 * g + 15) % 30;
                j = (year + year / 4 + i) % 7;
            }
            int l = i - j;
            int m = 3 + (l + 40) / 44; // 1-based month in which Easter falls
            int d = l + 28 - 31 * (m / 4); // Date of Easter within that month

            cal.clear();
            cal.set(Calendar.ERA, GregorianCalendar.AD);
            cal.set(Calendar.YEAR, year);
            cal.set(Calendar.MONTH, m - 1); // 0-based
            cal.set(Calendar.DATE, d);
            cal.getTime(); // JDK 1.1.2 bug workaround
            cal.add(Calendar.DATE, daysAfterEaster);

            return cal.getTime();
        }
    }

    private int daysAfterEaster;
    private GregorianCalendar calendar = new GregorianCalendar();
}
